跳到主要内容

第二章 MDK在线调试与代码规范

使用在线调试的方式进行STM32程序开发,可以直观实时观察程序中变量的变化情况,快速定位程序逻辑错误。通过设置断点、单步执行、查看寄存器和内存等手段,能够深入分析程序运行状态。在调试过程中,应遵循代码规范,合理命名变量与函数,添加必要的注释,确保代码可读性与可维护性。同时,使用一致的缩进和代码结构,有助于提升团队协作效率,并降低后期维护成本。

2.1MDK强大的在线调试功能

打开上一章的作业答案,即点亮全部小灯的程序的工程文件。然后在Keil软件界面,单击“Start/Stop Debug Session”按钮,软件将自动下载当前最新编译的程序代码,并且进入到调试模式,如图2-1所示。

图2-1  进入Keil在线调试模式

Keil在调试模式下,共分为8个区域,如图2-2所示。

图2-2  调试模式下的窗口信息

图2-1用标记规划了8个区域,其中:

  1. 控制程序运行区域,可以实现复位、单步运行、进入函数等操作。
  2. 工具快捷方式栏,可以通过勾选,查看内存、变量、单片机外设寄存器数值等。
  3. 在线控制区域,可以点击退出在线调试,也可以设置断点、取消断点等操作。
  4. 汇编代码界面。
  5. 单片机CM3内核的寄存器界面。
  6. 程序界面。
  7. 信息输出界面。
  8. 内存、变量、栈等信息查看界面。

2.2程序单步运行

调试模式下的区域1,从左往右第一个按钮是“复位”,单击它,单片机会自动复位,并停止等待下一步操作;第二个按钮是“全速运行/继续执行”,单击它,会让单片机全速运行/继续执行,直至遇到断点才会停下;第三个按钮是“停止执行”,单击它,能够让程序立刻停止运行。带大括号的这四个按钮分别是“单步执行(遇到函数进入函数)”、“逐步运行(遇到函数跳过函数)”、“跳出运行(跳出本函数)”和“运行至当前光标位置”。最右侧黄色箭头表示让区域6显示程序当前停止的位置,如图2-3所示。

图2-3 程序运行控制按钮

例如在程序95行代码前,单击鼠标在灰色位置添加一个断点,单击“RST”按钮,然后单击“运行”按钮,此时程序运行到95行停止,如图2-4所示。

图2-4  程序全速运行状态

如果想进一步查看函数内部的情况,可以点击“进入函数按钮”,窗口就会自动打开函数的实现位置,并且黄色箭头表示当前程序运行在函数的最开始,如图2-5所示。

图2-5 程序进入函数

通过单击“逐步运行(遇到函数跳过函数)”或者“跳出运行(跳出本函数)”都可以看到相对应的操作执行。

2.3变量的查看

通过单步调试的目的,如同慢放录像,是为了让高速运行的程序慢下来,以便用户能够逐步观察和解剖其内部状态。当用户将程序停留在某一行代码位置时,光标停留,将鼠标放在此处,就会浮现一个黄色的提示窗口,通过窗口信息可获取变量或者寄存器的值,如图2-6所示,PinState的值是0x00。

图2-6 查看变量

鼠标右键点击变量“PinState”,然后选择“Add‘PinState’to...”,再选择“Watch 1”,就可以在信息查看窗口(区域8)看到“PinState”这个变量被添加进来。此时程序单步运行,或者停止,都可以在“Watch 1”窗口下查看“PinState”实时值。请注意,要想查看局部变量的值,程序需要停在函数内,如图2-7所示。

图2-7 信息查看窗口

勾选View菜单下的“Periodic Window Update”,变量就会在运行时实时更新,多数情况下查看变量是单步运行时查看,如图2-8所示。

图2-8 实时变量更新配置选项

2.4函数的调用栈

2.4.1栈的概述

可以把栈理解成一个“临时储物架”,这个储物架(栈)是RAM种一块预留的、连续的区域,专门用于存储函数的临时数据,它有以下两个特点:

  1. 自动管理:这个储物架的分配和回收是由编译器和单片机硬件自动完成,用户通常不需要去干预。
  2. 先进后出:就像弹夹一样,最新压入的子弹压在下方,射击的时候最后压入的子弹被先击发。

2.4.2栈的核心作用

  1. 存储函数的局部变量。在函数内部声明的非静态局部变量,它的内存空间就直接在栈上那个区域分配。

  2. 保存函数调用的现场。当程序产生函数调用,调用函数结束时,应该返回到调用之前的哪条语句继续执行,这个过程就由栈来协助完成。 在跳转到被调用函数之前,单片机自动将返回地址压入栈中,并且还会将一些寄存器或者局部变量的值压入栈中,防止被调用函数修改。函数返回时,单片机从栈中弹出之前保存的地址,加载到程序计数器中,从而跳转回继续执行。

  3. 有些情况下,当前函数调用另外一个函数时,当前函数有时候也会将要传递的参数压入栈中,被调用函数再从栈中取出参数。

  4. 保存中断上下文。在单片机中,当中断发生时,无论程序当前在执行什么,都必须暂停去处理中断服务程序(ISR),为了确保中断处理完毕能准确恢复被中断的程序,单片机会自动将关键的寄存器的值压入栈中,等ISR执行完成后,再从栈中弹出这些值,恢复现场,继续执行被中断的程序。

  5. 当函数的返回值太大的时候,有时候编译器会在栈上开辟一块空间来存放这个返回值。

2.4.3栈的常见问题与注意事项

  1. 栈溢出。这是最常见的栈相关问题,当函数调用层级过深或局部变量占用空间过大时,可能导致栈空间耗尽,程序崩溃或行为异常。为避免栈溢出,应合理控制递归深度,减少大尺寸局部变量的使用,并根据系统资源合理配置栈大小。 在图2-2的区域8中,点击“Call Stack Locals”这个窗口,可以看到函数调用栈的情况。其中“Location/Value”代表的是函数的栈中变量的位置和值等相关信息,如图2-9所示。

图2-9 调用栈信息

从图2-9中还可以看出,main函数是在最底层,说明是最先压入栈中的,其次是MX_GPIO_Init,最后HAL_GPIO_WriterPin函数在最上层。可以得出结论是main主函数调用了MX_GPIO_Init函数,MX_GPIO_Init函数调用了HAL_GPIO_WriterPin函数。 假设有一段代码,程序循环为数组num的每一位进行加1操作,如下所示。

int num[10]={0};
int main(void)
{
int i=0;
while (1)
{
num[i]=i;
i++;//数组越界代码
}
}

这段代码定义的数组只有10个元素,而在main函数执行的时候,i不停进行加1操作,最终会导致数组越界溢出,这个时候会产生一个严重错误中断HardFault_Handler。 首先点击程序暂停,可以看到汇编代码中黄色箭头位置,鼠标点击对应的函数名字MemManage_Handler(void),在C语言窗口就会自动定位到该C语言函数的位置HardFault_Handler(void)HardFault_Handler的“HardFault”代表的是严重错误,而“Handler”是处理程序的意思。“Handler”本身有“操作者”、“处理函数”“处理程序”等意思,意思是对应“HardFault”的处理函数。在操作系统或者某些程序语言中,Handler本身可以理解成像指针一样的东西,一旦发生对应事件需要调用某些对应功能的函数或者内存。“HardFault_Handler”的意思是一旦发生严重错误,就进入严重错误处理函数,一般这种函数是系统的库函数。 HardFault_Handler错误最常见的就是栈溢出等内存处理错误。打开“Call Stack Locals”窗口,可以看到栈的使用情况,并且可以看出是main函数在运行过程中出现的错误。通过仔细查看main函数可以发现i如果一直加,就会超出数组的边界,如图2-10所示。

图2-10 栈错误查找 通过修改main函数程序就可以解决这个问题。但是细心的读者是否发现,错误出现时,i的取值已经来到了16356,远远超越了数组的边界,达到了栈的RAM边界才引发这个严重错误的中断。如果用户的程序比数组边界大,但是没有达到栈的边界,依然不会引发这个错误中断。当数组越界修改了其他变量的内容,就会导致程序不受控制,此时依然可以通过查看栈的情况,比如但不观察i的取值来了解程序运行的情况,最终查找到问题所在并且解决问题,如图2-11所示。

图2-11 栈数据查看

在程序中的num这个数组名位置点右键,将其添加到Watch1窗口,可以看到数组内的变化情况,也可以将数组的地址0x2000070复制到内存查看器Memory1中查看具体情况,如图2-12所示。

图2-11 通过Watch窗口查看变量的值 通过Memory1窗口可以看到数组后边的内存数据也被越界更改了(int类型数组num的每个元素占4个字节)。所以在使用数组或者指针等操作时,一定要考虑其安全性,避免越界的情况发生,如图2-12所示。

图2-12 通过Memory1窗口查看单片机内存情况 2.5外设寄存器查看 单片机的内部外设值的情况也可以通过“view”菜单下的“System Viewer”窗口,或者直接点击“System Viewer”窗口图标,就可以实时查看单片机相关内部外设的寄存器的值,如图2-13所示。

图2-13 System Viewer窗口寄存器 如果点击GPIO下的GPIOA图标,就可以看到GPIOA的寄存器情况,如图2-14所示。

图2-14 GPIOA寄存器情况 2.6CubeMX生成的HAL工程代码规范 CubeMX生成的工程,一般分为以下几个组别,如图2-15所示。其中“APPlication/MDK-ARM”存放STM32的启动文件,“APPlication/User/Core”存放用户的代码,“Dirvers/STM32F1XX_HAL_Dirver”存放HAL库的驱动代码,“Dirvers/CMSIS”用于存放系统初始化文件。

图2-15 CubeMX生成的工程代码文件列表 2.6.1startup_stm32f103xe.s 这个文件是STM32的启动文件,使用汇编语言编写的。这个文件包含了STM32启动初始化和中断向量如何等信息,通常情况下,用户不必要修改和关注这个文件的内容。 2.6.2main.c main.c文件主要存放main函数和内部外设的初始化函数,这也是用户代码实现的主要文件。可以看到在CubeMX生成的代码中,包含了大量的注释,并且置顶了多个位置让用户使用,如下所示。

/*Includes-------------------------------------------------------------*/
#include "main.h"

/*Private includes-----------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/*Private typedef------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/*Private define-------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/*Private macro--------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/*Private variables----------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

请注意,用户的功能程序代码必须写到/* USER CODE BEGIN XXX */和/* USER CODE END XXX */之间,如果不这样做的话,重新打开CubeMX添加新的一些引脚或者外设配置,CubeMX重新生成代码时会把用户代码之外的全部清除掉,代码区域注塑如表2-1所示。 表2-1 用户代码区域解析

区域解释举例
Private includes私有头文件区#include <stdlib.h>
Private typedef私有数据类型区typedef unsigned int uint32_t;
Private define私有定义区域#define USE_FREERTOS 1
Private macro私有宏区域#define MAX(a, b) (((a) > (b)) ? (a) : (b))
Private variables私有全局变量int buff[10];

2.6.3 stm32f1xx_it.c

这个文件中有很多类似void XXXX_Handler(void)的函数,这个函数与51单片机的中断函数void InterruptTimer0() interrupt 1是类似的。在51单片机中,编译器通过识别“interrupt”这个关键字来确认这个函数是中断函数,链接的时候,通过“1”进行函数地址链接。在STM32的中断程序中,HAL库已经将所有的中断进行接管,并且做了一定的处理机制,用户处理中断时,只需要在自己的文件中编写对应的回调函数(通过函数指针调用函数)。

2.6.4 stm32f1xx_hal_msp.c

这个文件是告诉HAL库关于硬件的配置和连接方式,需要如何初始化。它是HAL库实现硬件底层驱动的文件。由于用户在CubeMX里已经配置了时钟、IO引脚等功能,因此这部分代码是由CubeMX图形界面配置完成后自动生成的。

2.6.5Drivers/STM32F1xx_HAL_Driver

这个文件夹下的文件是STM32F1系列单片机所有外设的通用驱动库。它提供了操作外设的标准化函数接口(API),不关心具体电路连了哪个引脚,哪个时钟源,他关注的是外设本身的功能操作。 这个文件夹可以理解是ST官方提供的“武器库”,他提供了各种外设(枪、炮,雷达)的操作接口(API)。只是告诉用户都有什么武器和基本操作方法,但是不告诉用户哪把枪装什么子弹(引脚),放在哪个位置上。

而“stm32f1xx_hal_msp.c”就是用户的“部署方案”,用户或者用程序,或者用CubeMX图形界面来部署哪个功能配置在哪个“枪”或者“炮弹”上(引脚配置),用的什么瞄准镜(中断有优先级)等等。

2.7规范代码编写

在CubeMX生成的HAL工程文件当中,用户编写代码需要遵循以下三个规范:

  1. 不修改HAL_Driver文件夹下的HAL库源代码,只在APPlication文件中进行修改。
  2. 修改main.c、stm32f1xx_it_.c、和stm32f1xxx_hal_msp.c文件时,遵循注释规范的位置,防止后续增加新功能时导致代码丢失。
  3. 用户自己的代码文件可以创建新的分组。

2.8课后作业

  1. 熟练掌握Keil软件在线调试功能。
  2. 了解HAL工程文件对应的功能。使用在线调试的方式进行STM32程序开发,可以直观实时观察程序中变量的变化情况,快速定位程序逻辑错误。通过设置断点、单步执行、查看寄存器和内存等手段,能够深入分析程序运行状态。在调试过程中,应遵循代码规范,合理命名变量与函数,添加必要的注释,确保代码可读性与可维护性。同时,使用一致的缩进和代码结构,有助于提升团队协作效率,并降低后期维护成本。